Skip to content

FAC-98 feat: add audit trail MVP#215

Merged
y4nder merged 11 commits intodevelopfrom
FAC-98/feat/add-audit-trail-mvp
Mar 29, 2026
Merged

FAC-98 feat: add audit trail MVP#215
y4nder merged 11 commits intodevelopfrom
FAC-98/feat/add-audit-trail-mvp

Conversation

@y4nder
Copy link
Copy Markdown
Member

@y4nder y4nder commented Mar 29, 2026

Summary

  • Add append-only AuditLog entity backed by a BullMQ AUDIT queue with dual emission paths (interceptor + direct emit)
  • Tag 11 MVP endpoints across auth, moodle sync, questionnaires, and analysis modules
  • Include full documentation: architecture doc, 4 decision records, workflow updates

Changes

New Files (14)

File Purpose
src/entities/audit-log.entity.ts Append-only entity (no CustomBaseEntity, no FK to User)
src/modules/audit/audit.module.ts @Global() module — cross-cutting concern
src/modules/audit/audit.service.ts Fire-and-forget Emit() with error swallowing
src/modules/audit/audit.processor.ts BullMQ processor, concurrency: 1, direct DB write
src/modules/audit/audit-action.enum.ts 12 MVP action constants
src/modules/audit/dto/audit-job-message.dto.ts Queue envelope interface
src/modules/audit/dto/emit-params.dto.ts Emit parameter interface
src/modules/audit/decorators/audited.decorator.ts @Audited() SetMetadata decorator
src/modules/audit/interceptors/audit.interceptor.ts RxJS tap() post-response emit
src/modules/audit/testing/audit-test.helpers.ts Shared test utilities for controller specs
src/modules/audit/audit.service.spec.ts Service unit tests
src/modules/audit/audit.processor.spec.ts Processor unit tests
src/modules/audit/interceptors/audit.interceptor.spec.ts Interceptor unit tests
docs/architecture/audit-trail.md Full architecture documentation

Modified Files (18)

  • src/configurations/common/queue-names.ts — Added AUDIT
  • src/entities/index.entity.ts — Registered AuditLog
  • src/modules/index.module.ts — Added AuditModule to ApplicationModules
  • src/modules/auth/auth.controller.ts — Tagged logout with @Audited()
  • src/modules/auth/auth.service.ts — Refactored Login/RefreshToken with direct audit emits outside transactions
  • src/modules/moodle/controllers/moodle-sync.controller.ts — Tagged sync trigger + schedule update
  • src/modules/questionnaires/questionnaire.controller.ts — Tagged submit, ingest, wipe
  • src/modules/analysis/analysis.controller.ts — Tagged pipeline create/confirm/cancel
  • src/modules/auth/auth.service.spec.ts — 7 new audit tests + @optional test
  • src/modules/moodle/controllers/moodle-sync.controller.spec.ts — Added interceptor mocks
  • src/modules/questionnaires/questionnaire.controller.spec.ts — Added interceptor mocks
  • src/modules/analysis/analysis.controller.spec.ts — Added interceptor mocks
  • docs/architecture/core-components.md — Added AuditModule, AUDIT queue
  • docs/decisions/decisions.md — Added decisions feat: implement automated releases and fix test reporting #34-37
  • docs/workflows/auth-hydration.md — Added audit events section

Key Design Decisions

  • No FK to UseractorId is a plain string so audit records survive user deletion
  • @Global() module — only application module to use this; avoids friction in adopting @Audited()
  • Dual paths — interceptor for standard endpoints, direct emit for auth events lacking CLS context
  • Sanitized metadata — login failure reasons use fixed codes, never raw error.message
  • Metadata cap — interceptor limits captured params/query to 4KB

Test plan

  • All 622 existing + new tests passing
  • ESLint clean
  • New tests: AuditService (4), AuditProcessor (5), AuditInterceptor (9), AuthService audit (7)
  • Adversarial review completed (15 findings, 9 fixed)
  • Verify migration applies cleanly on fresh database
  • Manual smoke test: login → check audit_log table for auth.login.success row

Closes #214

🤖 Generated with Claude Code

claude and others added 11 commits March 29, 2026 18:18
Initialize tech-spec for audit trail feature covering auth events,
admin actions, and sensitive data mutations via BullMQ queue.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Add detailed file references, code patterns, MVP endpoint targets,
and testing strategy from Step 2 codebase investigation.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Add 20 tasks across 5 phases, 11 acceptance criteria, testing strategy,
and risk analysis. Step 3 of quick-spec workflow complete.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Rename WIP to tech-spec-audit-trail-mvp.md and mark status
as ready-for-dev. Quick-spec workflow complete (steps 1-4).

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Add 6 preventions from pre-mortem analysis: @optional() AuditService
injection in AuthService, Logger.warn on failed emissions, attempts:1
on audit jobs, CLS null-safety warnings, bulk ingestion documentation,
and full payload logging on processor failures.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Log full emission params on failure, add @optional() undefined test
case for auth service, add CLS null-metadata warning test for interceptor.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Critical: @global() AuditModule, explicit interceptor stacks on all
MVP endpoints, fix DI scoping for host modules.
High: move login/refresh to direct emit, extend @Audited() with
resource option, add AuditAction enum, tap vs finalize docs.
Medium: drop repo stub, fix occurredAt, add attempts:1 in code,
separate browserName/os fields, fix role claim, complete file list.
Low: clarify emit location, entity array registration, test Redis,
interceptor ordering, endpoint count.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Fix 14 findings: emit audit AFTER transactions with void (fire-and-
forget), replace existing @UseInterceptors instead of adding duplicates,
AuditInterceptor falls back to JWT payload (no CurrentUserInterceptor
required), import AppClsModule instead of CommonModule, PII-safe
logging in failed handler, PostgreSQL DEFAULT on occurred_at, fix AC 4
syntax and Task 6 Zod contradiction, document empty metadata for
/ingest and null resourceId for created-by-action scenarios.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Critical: add explicit transaction refactoring pattern with code
snippets for Login() and RefreshToken() — emit after transaction
returns, not inside. High: correct @optional() resilience claim,
fix resourceType for wipe endpoint, add per-endpoint interceptor
bullets, acknowledge logout MetaDataInterceptor gap. Medium: add
metadata schema table, full UUID regex, AuthenticatedRequest type,
justify @global() convention, full FileInterceptor config, fix
/ingest CurrentUserInterceptor contradiction. Low: create()+flush()
pattern, processor log counter, fix test description, base class name.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Fix metadata table accuracy (4 rows showed wrong shapes), add null-
safe destructure for RequestMetadataService in direct emit, strategy
Name fallback for minification, type action as AuditAction end-to-end,
consistent PII redaction policy, add CurrentUserInterceptor to POST
/submissions (highest-volume), make occurredAt required (no JS default),
document MetaDataInterceptor DI resolution, clarify attempts config,
note create-action resourceId gap, Redis failed job monitoring guidance,
actorId is plain string not @manytoone, logging side effect note.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP
Implement append-only AuditLog entity backed by a BullMQ AUDIT queue
with dual emission paths — interceptor for authenticated endpoints and
direct emit for auth events where CLS context is unavailable.

- AuditLog entity (no CustomBaseEntity, no soft delete, no FK to User)
- AuditService with fire-and-forget Emit() that never breaks requests
- @Audited() decorator + AuditInterceptor (RxJS tap, post-response)
- @global() AuditModule — cross-cutting concern, no explicit imports
- 11 MVP endpoints tagged: auth (login/logout/refresh), moodle sync,
  questionnaire (submit/ingest/wipe), analysis (create/confirm/cancel)
- Sanitized failure metadata (fixed reason codes, no raw error.message)
- Metadata size cap (4KB) on interceptor-captured params/query
- actorId index for "what did user X do?" queries
- Shared audit test helpers to DRY controller spec boilerplate
- 622 tests passing, lint clean
@y4nder y4nder linked an issue Mar 29, 2026 that may be closed by this pull request
@y4nder y4nder self-assigned this Mar 29, 2026
@y4nder y4nder merged commit 5c3a9a4 into develop Mar 29, 2026
2 checks passed
y4nder added a commit that referenced this pull request Mar 30, 2026
* docs: add audit trail MVP tech-spec WIP

Initialize tech-spec for audit trail feature covering auth events,
admin actions, and sensitive data mutations via BullMQ queue.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: update audit trail tech-spec with deep investigation findings

Add detailed file references, code patterns, MVP endpoint targets,
and testing strategy from Step 2 codebase investigation.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: generate full implementation plan for audit trail tech-spec

Add 20 tasks across 5 phases, 11 acceptance criteria, testing strategy,
and risk analysis. Step 3 of quick-spec workflow complete.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: finalize audit trail MVP tech-spec as ready-for-dev

Rename WIP to tech-spec-audit-trail-mvp.md and mark status
as ready-for-dev. Quick-spec workflow complete (steps 1-4).

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply pre-mortem hardening to audit trail tech-spec

Add 6 preventions from pre-mortem analysis: @optional() AuditService
injection in AuthService, Logger.warn on failed emissions, attempts:1
on audit jobs, CLS null-safety warnings, bulk ingestion documentation,
and full payload logging on processor failures.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: add team refinements to audit trail tech-spec

Log full emission params on failure, add @optional() undefined test
case for auth service, add CLS null-metadata warning test for interceptor.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply all 23 adversarial review fixes to audit trail spec

Critical: @global() AuditModule, explicit interceptor stacks on all
MVP endpoints, fix DI scoping for host modules.
High: move login/refresh to direct emit, extend @Audited() with
resource option, add AuditAction enum, tap vs finalize docs.
Medium: drop repo stub, fix occurredAt, add attempts:1 in code,
separate browserName/os fields, fix role claim, complete file list.
Low: clarify emit location, entity array registration, test Redis,
interceptor ordering, endpoint count.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 2 adversarial review fixes to audit trail spec

Fix 14 findings: emit audit AFTER transactions with void (fire-and-
forget), replace existing @UseInterceptors instead of adding duplicates,
AuditInterceptor falls back to JWT payload (no CurrentUserInterceptor
required), import AppClsModule instead of CommonModule, PII-safe
logging in failed handler, PostgreSQL DEFAULT on occurred_at, fix AC 4
syntax and Task 6 Zod contradiction, document empty metadata for
/ingest and null resourceId for created-by-action scenarios.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 3 adversarial review fixes to audit trail spec

Critical: add explicit transaction refactoring pattern with code
snippets for Login() and RefreshToken() — emit after transaction
returns, not inside. High: correct @optional() resilience claim,
fix resourceType for wipe endpoint, add per-endpoint interceptor
bullets, acknowledge logout MetaDataInterceptor gap. Medium: add
metadata schema table, full UUID regex, AuthenticatedRequest type,
justify @global() convention, full FileInterceptor config, fix
/ingest CurrentUserInterceptor contradiction. Low: create()+flush()
pattern, processor log counter, fix test description, base class name.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 4 adversarial review fixes to audit trail spec

Fix metadata table accuracy (4 rows showed wrong shapes), add null-
safe destructure for RequestMetadataService in direct emit, strategy
Name fallback for minification, type action as AuditAction end-to-end,
consistent PII redaction policy, add CurrentUserInterceptor to POST
/submissions (highest-volume), make occurredAt required (no JS default),
document MetaDataInterceptor DI resolution, clarify attempts config,
note create-action resourceId gap, Redis failed job monitoring guidance,
actorId is plain string not @manytoone, logging side effect note.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* FAC-98 feat: add audit trail MVP

Implement append-only AuditLog entity backed by a BullMQ AUDIT queue
with dual emission paths — interceptor for authenticated endpoints and
direct emit for auth events where CLS context is unavailable.

- AuditLog entity (no CustomBaseEntity, no soft delete, no FK to User)
- AuditService with fire-and-forget Emit() that never breaks requests
- @Audited() decorator + AuditInterceptor (RxJS tap, post-response)
- @global() AuditModule — cross-cutting concern, no explicit imports
- 11 MVP endpoints tagged: auth (login/logout/refresh), moodle sync,
  questionnaire (submit/ingest/wipe), analysis (create/confirm/cancel)
- Sanitized failure metadata (fixed reason codes, no raw error.message)
- Metadata size cap (4KB) on interceptor-captured params/query
- actorId index for "what did user X do?" queries
- Shared audit test helpers to DRY controller spec boilerplate
- 622 tests passing, lint clean

---------

Co-authored-by: Claude <noreply@anthropic.com>
y4nder added a commit that referenced this pull request Mar 30, 2026
… Trail MVP (#217)

* chore: add /audit skill with auto GitHub issue creation

* FAC-87 FAC-88 fix: async bcrypt and expired token filtering in RefreshToken #199

Replace bcrypt.compareSync with concurrent async bcrypt.compare via
Promise.all to avoid blocking the event loop (#195). Add expiresAt
filter to the DB query so expired tokens are excluded before running
expensive bcrypt comparisons (#197).

Closes #195
Closes #197

https://claude.ai/code/session_01XttX7NLr6HY7TDLddDk2AG

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-89 fix: username logged in plaintext in Moodle login error path #200

Closes #198

* FAC-90 fix: LoginRequest DTO missing @isnotempty and length constraints #201

Empty strings and arbitrarily long payloads were passing validation on
the login endpoint. Add @isnotempty() and @maxlength() decorators to
both username and password fields, consistent with RefreshTokenRequestBody.
Add DTO validation tests covering empty and oversized inputs.

Closes #196

* FAC-91 FAC-92 fix: include teacher role in faculty lookup and use EnrollmentRole enum #206

getFacultyByCourseIds() only queried 'editingteacher', missing faculty with the
'teacher' Moodle role. Now queries both roles via the EnrollmentRole enum,
matching the pattern already used in FacultyService.

Closes #202, Closes #203

https://claude.ai/code/session_01LNfjqKQstgZ7skvJ2oXAkf

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-93 perf: limit fields in getFacultyByCourseIds query #207

Drop eager Course populate (FK already on Enrollment) and restrict User
populate to the 5 columns actually used in the mapping, reducing DB I/O.

https://claude.ai/code/session_01U2k8WWJ9EJ9b3Rzg4Vq81S

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-94 refactor: enrollment test relies on implicit Promise.all mock call order #208

Replace mockResolvedValueOnce chains with mockImplementation that matches
on entity class (Enrollment vs QuestionnaireSubmission), making the test
resilient to Promise.all reordering in the service.

Closes #205

https://claude.ai/code/session_012UsMjq61dUvFhErpChg9cR

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-95 FAC-96 refactor: curriculum service filter type safety and search deduplication#212

Replace `Record<string, unknown>` with typed `FilterQuery<Department>` and
`FilterQuery<Program>` intermediates, removing all `as FilterQuery<T>` casts
in ListPrograms and ListCourses (#210).

Extract duplicated $and/$or/$ilike search block into a shared
ApplySearchFilter helper used by all three list methods (#211).

Closes #210
Closes #211

https://claude.ai/code/session_01Y6eWhKjmgLfPJQMJa4VQQB

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-97 perf: add pagination to curriculum list endpoints#213

Curriculum list endpoints (/departments, /programs, /courses) returned
unbounded result sets. Add page/limit query params via PaginationQueryDto
and return paginated { data, meta } responses using findAndCount, matching
the pattern used by admin, faculty, and dimensions modules.

https://claude.ai/code/session_01Ug6JLCMPhy42Skcq7tWyVV

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-98 feat: add audit trail MVP (#215)

* docs: add audit trail MVP tech-spec WIP

Initialize tech-spec for audit trail feature covering auth events,
admin actions, and sensitive data mutations via BullMQ queue.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: update audit trail tech-spec with deep investigation findings

Add detailed file references, code patterns, MVP endpoint targets,
and testing strategy from Step 2 codebase investigation.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: generate full implementation plan for audit trail tech-spec

Add 20 tasks across 5 phases, 11 acceptance criteria, testing strategy,
and risk analysis. Step 3 of quick-spec workflow complete.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: finalize audit trail MVP tech-spec as ready-for-dev

Rename WIP to tech-spec-audit-trail-mvp.md and mark status
as ready-for-dev. Quick-spec workflow complete (steps 1-4).

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply pre-mortem hardening to audit trail tech-spec

Add 6 preventions from pre-mortem analysis: @optional() AuditService
injection in AuthService, Logger.warn on failed emissions, attempts:1
on audit jobs, CLS null-safety warnings, bulk ingestion documentation,
and full payload logging on processor failures.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: add team refinements to audit trail tech-spec

Log full emission params on failure, add @optional() undefined test
case for auth service, add CLS null-metadata warning test for interceptor.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply all 23 adversarial review fixes to audit trail spec

Critical: @global() AuditModule, explicit interceptor stacks on all
MVP endpoints, fix DI scoping for host modules.
High: move login/refresh to direct emit, extend @Audited() with
resource option, add AuditAction enum, tap vs finalize docs.
Medium: drop repo stub, fix occurredAt, add attempts:1 in code,
separate browserName/os fields, fix role claim, complete file list.
Low: clarify emit location, entity array registration, test Redis,
interceptor ordering, endpoint count.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 2 adversarial review fixes to audit trail spec

Fix 14 findings: emit audit AFTER transactions with void (fire-and-
forget), replace existing @UseInterceptors instead of adding duplicates,
AuditInterceptor falls back to JWT payload (no CurrentUserInterceptor
required), import AppClsModule instead of CommonModule, PII-safe
logging in failed handler, PostgreSQL DEFAULT on occurred_at, fix AC 4
syntax and Task 6 Zod contradiction, document empty metadata for
/ingest and null resourceId for created-by-action scenarios.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 3 adversarial review fixes to audit trail spec

Critical: add explicit transaction refactoring pattern with code
snippets for Login() and RefreshToken() — emit after transaction
returns, not inside. High: correct @optional() resilience claim,
fix resourceType for wipe endpoint, add per-endpoint interceptor
bullets, acknowledge logout MetaDataInterceptor gap. Medium: add
metadata schema table, full UUID regex, AuthenticatedRequest type,
justify @global() convention, full FileInterceptor config, fix
/ingest CurrentUserInterceptor contradiction. Low: create()+flush()
pattern, processor log counter, fix test description, base class name.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 4 adversarial review fixes to audit trail spec

Fix metadata table accuracy (4 rows showed wrong shapes), add null-
safe destructure for RequestMetadataService in direct emit, strategy
Name fallback for minification, type action as AuditAction end-to-end,
consistent PII redaction policy, add CurrentUserInterceptor to POST
/submissions (highest-volume), make occurredAt required (no JS default),
document MetaDataInterceptor DI resolution, clarify attempts config,
note create-action resourceId gap, Redis failed job monitoring guidance,
actorId is plain string not @manytoone, logging side effect note.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* FAC-98 feat: add audit trail MVP

Implement append-only AuditLog entity backed by a BullMQ AUDIT queue
with dual emission paths — interceptor for authenticated endpoints and
direct emit for auth events where CLS context is unavailable.

- AuditLog entity (no CustomBaseEntity, no soft delete, no FK to User)
- AuditService with fire-and-forget Emit() that never breaks requests
- @Audited() decorator + AuditInterceptor (RxJS tap, post-response)
- @global() AuditModule — cross-cutting concern, no explicit imports
- 11 MVP endpoints tagged: auth (login/logout/refresh), moodle sync,
  questionnaire (submit/ingest/wipe), analysis (create/confirm/cancel)
- Sanitized failure metadata (fixed reason codes, no raw error.message)
- Metadata size cap (4KB) on interceptor-captured params/query
- actorId index for "what did user X do?" queries
- Shared audit test helpers to DRY controller spec boilerplate
- 622 tests passing, lint clean

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
y4nder added a commit that referenced this pull request Mar 30, 2026
… Trail MVP (#217)

* chore: add /audit skill with auto GitHub issue creation

* FAC-87 FAC-88 fix: async bcrypt and expired token filtering in RefreshToken #199

Replace bcrypt.compareSync with concurrent async bcrypt.compare via
Promise.all to avoid blocking the event loop (#195). Add expiresAt
filter to the DB query so expired tokens are excluded before running
expensive bcrypt comparisons (#197).

Closes #195
Closes #197

https://claude.ai/code/session_01XttX7NLr6HY7TDLddDk2AG

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-89 fix: username logged in plaintext in Moodle login error path #200

Closes #198

* FAC-90 fix: LoginRequest DTO missing @isnotempty and length constraints #201

Empty strings and arbitrarily long payloads were passing validation on
the login endpoint. Add @isnotempty() and @maxlength() decorators to
both username and password fields, consistent with RefreshTokenRequestBody.
Add DTO validation tests covering empty and oversized inputs.

Closes #196

* FAC-91 FAC-92 fix: include teacher role in faculty lookup and use EnrollmentRole enum #206

getFacultyByCourseIds() only queried 'editingteacher', missing faculty with the
'teacher' Moodle role. Now queries both roles via the EnrollmentRole enum,
matching the pattern already used in FacultyService.

Closes #202, Closes #203

https://claude.ai/code/session_01LNfjqKQstgZ7skvJ2oXAkf

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-93 perf: limit fields in getFacultyByCourseIds query #207

Drop eager Course populate (FK already on Enrollment) and restrict User
populate to the 5 columns actually used in the mapping, reducing DB I/O.

https://claude.ai/code/session_01U2k8WWJ9EJ9b3Rzg4Vq81S

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-94 refactor: enrollment test relies on implicit Promise.all mock call order #208

Replace mockResolvedValueOnce chains with mockImplementation that matches
on entity class (Enrollment vs QuestionnaireSubmission), making the test
resilient to Promise.all reordering in the service.

Closes #205

https://claude.ai/code/session_012UsMjq61dUvFhErpChg9cR

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-95 FAC-96 refactor: curriculum service filter type safety and search deduplication#212

Replace `Record<string, unknown>` with typed `FilterQuery<Department>` and
`FilterQuery<Program>` intermediates, removing all `as FilterQuery<T>` casts
in ListPrograms and ListCourses (#210).

Extract duplicated $and/$or/$ilike search block into a shared
ApplySearchFilter helper used by all three list methods (#211).

Closes #210
Closes #211

https://claude.ai/code/session_01Y6eWhKjmgLfPJQMJa4VQQB

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-97 perf: add pagination to curriculum list endpoints#213

Curriculum list endpoints (/departments, /programs, /courses) returned
unbounded result sets. Add page/limit query params via PaginationQueryDto
and return paginated { data, meta } responses using findAndCount, matching
the pattern used by admin, faculty, and dimensions modules.

https://claude.ai/code/session_01Ug6JLCMPhy42Skcq7tWyVV

Co-authored-by: Claude <noreply@anthropic.com>

* FAC-98 feat: add audit trail MVP (#215)

* docs: add audit trail MVP tech-spec WIP

Initialize tech-spec for audit trail feature covering auth events,
admin actions, and sensitive data mutations via BullMQ queue.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: update audit trail tech-spec with deep investigation findings

Add detailed file references, code patterns, MVP endpoint targets,
and testing strategy from Step 2 codebase investigation.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: generate full implementation plan for audit trail tech-spec

Add 20 tasks across 5 phases, 11 acceptance criteria, testing strategy,
and risk analysis. Step 3 of quick-spec workflow complete.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: finalize audit trail MVP tech-spec as ready-for-dev

Rename WIP to tech-spec-audit-trail-mvp.md and mark status
as ready-for-dev. Quick-spec workflow complete (steps 1-4).

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply pre-mortem hardening to audit trail tech-spec

Add 6 preventions from pre-mortem analysis: @optional() AuditService
injection in AuthService, Logger.warn on failed emissions, attempts:1
on audit jobs, CLS null-safety warnings, bulk ingestion documentation,
and full payload logging on processor failures.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: add team refinements to audit trail tech-spec

Log full emission params on failure, add @optional() undefined test
case for auth service, add CLS null-metadata warning test for interceptor.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply all 23 adversarial review fixes to audit trail spec

Critical: @global() AuditModule, explicit interceptor stacks on all
MVP endpoints, fix DI scoping for host modules.
High: move login/refresh to direct emit, extend @Audited() with
resource option, add AuditAction enum, tap vs finalize docs.
Medium: drop repo stub, fix occurredAt, add attempts:1 in code,
separate browserName/os fields, fix role claim, complete file list.
Low: clarify emit location, entity array registration, test Redis,
interceptor ordering, endpoint count.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 2 adversarial review fixes to audit trail spec

Fix 14 findings: emit audit AFTER transactions with void (fire-and-
forget), replace existing @UseInterceptors instead of adding duplicates,
AuditInterceptor falls back to JWT payload (no CurrentUserInterceptor
required), import AppClsModule instead of CommonModule, PII-safe
logging in failed handler, PostgreSQL DEFAULT on occurred_at, fix AC 4
syntax and Task 6 Zod contradiction, document empty metadata for
/ingest and null resourceId for created-by-action scenarios.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 3 adversarial review fixes to audit trail spec

Critical: add explicit transaction refactoring pattern with code
snippets for Login() and RefreshToken() — emit after transaction
returns, not inside. High: correct @optional() resilience claim,
fix resourceType for wipe endpoint, add per-endpoint interceptor
bullets, acknowledge logout MetaDataInterceptor gap. Medium: add
metadata schema table, full UUID regex, AuthenticatedRequest type,
justify @global() convention, full FileInterceptor config, fix
/ingest CurrentUserInterceptor contradiction. Low: create()+flush()
pattern, processor log counter, fix test description, base class name.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 4 adversarial review fixes to audit trail spec

Fix metadata table accuracy (4 rows showed wrong shapes), add null-
safe destructure for RequestMetadataService in direct emit, strategy
Name fallback for minification, type action as AuditAction end-to-end,
consistent PII redaction policy, add CurrentUserInterceptor to POST
/submissions (highest-volume), make occurredAt required (no JS default),
document MetaDataInterceptor DI resolution, clarify attempts config,
note create-action resourceId gap, Redis failed job monitoring guidance,
actorId is plain string not @manytoone, logging side effect note.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* FAC-98 feat: add audit trail MVP

Implement append-only AuditLog entity backed by a BullMQ AUDIT queue
with dual emission paths — interceptor for authenticated endpoints and
direct emit for auth events where CLS context is unavailable.

- AuditLog entity (no CustomBaseEntity, no soft delete, no FK to User)
- AuditService with fire-and-forget Emit() that never breaks requests
- @Audited() decorator + AuditInterceptor (RxJS tap, post-response)
- @global() AuditModule — cross-cutting concern, no explicit imports
- 11 MVP endpoints tagged: auth (login/logout/refresh), moodle sync,
  questionnaire (submit/ingest/wipe), analysis (create/confirm/cancel)
- Sanitized failure metadata (fixed reason codes, no raw error.message)
- Metadata size cap (4KB) on interceptor-captured params/query
- actorId index for "what did user X do?" queries
- Shared audit test helpers to DRY controller spec boilerplate
- 622 tests passing, lint clean

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
y4nder added a commit that referenced this pull request Mar 30, 2026
…agination & Audit Trail MVP (#218)

* chore: add /audit skill with auto GitHub issue creation

* FAC-87 FAC-88 fix: async bcrypt and expired token filtering in RefreshToken #199

Replace bcrypt.compareSync with concurrent async bcrypt.compare via
Promise.all to avoid blocking the event loop (#195). Add expiresAt
filter to the DB query so expired tokens are excluded before running
expensive bcrypt comparisons (#197).

Closes #195
Closes #197

https://claude.ai/code/session_01XttX7NLr6HY7TDLddDk2AG



* FAC-89 fix: username logged in plaintext in Moodle login error path #200

Closes #198

* FAC-90 fix: LoginRequest DTO missing @isnotempty and length constraints #201

Empty strings and arbitrarily long payloads were passing validation on
the login endpoint. Add @isnotempty() and @maxlength() decorators to
both username and password fields, consistent with RefreshTokenRequestBody.
Add DTO validation tests covering empty and oversized inputs.

Closes #196

* FAC-91 FAC-92 fix: include teacher role in faculty lookup and use EnrollmentRole enum #206

getFacultyByCourseIds() only queried 'editingteacher', missing faculty with the
'teacher' Moodle role. Now queries both roles via the EnrollmentRole enum,
matching the pattern already used in FacultyService.

Closes #202, Closes #203

https://claude.ai/code/session_01LNfjqKQstgZ7skvJ2oXAkf



* FAC-93 perf: limit fields in getFacultyByCourseIds query #207

Drop eager Course populate (FK already on Enrollment) and restrict User
populate to the 5 columns actually used in the mapping, reducing DB I/O.

https://claude.ai/code/session_01U2k8WWJ9EJ9b3Rzg4Vq81S



* FAC-94 refactor: enrollment test relies on implicit Promise.all mock call order #208

Replace mockResolvedValueOnce chains with mockImplementation that matches
on entity class (Enrollment vs QuestionnaireSubmission), making the test
resilient to Promise.all reordering in the service.

Closes #205

https://claude.ai/code/session_012UsMjq61dUvFhErpChg9cR



* FAC-95 FAC-96 refactor: curriculum service filter type safety and search deduplication#212

Replace `Record<string, unknown>` with typed `FilterQuery<Department>` and
`FilterQuery<Program>` intermediates, removing all `as FilterQuery<T>` casts
in ListPrograms and ListCourses (#210).

Extract duplicated $and/$or/$ilike search block into a shared
ApplySearchFilter helper used by all three list methods (#211).

Closes #210
Closes #211

https://claude.ai/code/session_01Y6eWhKjmgLfPJQMJa4VQQB



* FAC-97 perf: add pagination to curriculum list endpoints#213

Curriculum list endpoints (/departments, /programs, /courses) returned
unbounded result sets. Add page/limit query params via PaginationQueryDto
and return paginated { data, meta } responses using findAndCount, matching
the pattern used by admin, faculty, and dimensions modules.

https://claude.ai/code/session_01Ug6JLCMPhy42Skcq7tWyVV



* FAC-98 feat: add audit trail MVP (#215)

* docs: add audit trail MVP tech-spec WIP

Initialize tech-spec for audit trail feature covering auth events,
admin actions, and sensitive data mutations via BullMQ queue.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: update audit trail tech-spec with deep investigation findings

Add detailed file references, code patterns, MVP endpoint targets,
and testing strategy from Step 2 codebase investigation.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: generate full implementation plan for audit trail tech-spec

Add 20 tasks across 5 phases, 11 acceptance criteria, testing strategy,
and risk analysis. Step 3 of quick-spec workflow complete.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: finalize audit trail MVP tech-spec as ready-for-dev

Rename WIP to tech-spec-audit-trail-mvp.md and mark status
as ready-for-dev. Quick-spec workflow complete (steps 1-4).

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply pre-mortem hardening to audit trail tech-spec

Add 6 preventions from pre-mortem analysis: @optional() AuditService
injection in AuthService, Logger.warn on failed emissions, attempts:1
on audit jobs, CLS null-safety warnings, bulk ingestion documentation,
and full payload logging on processor failures.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: add team refinements to audit trail tech-spec

Log full emission params on failure, add @optional() undefined test
case for auth service, add CLS null-metadata warning test for interceptor.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply all 23 adversarial review fixes to audit trail spec

Critical: @global() AuditModule, explicit interceptor stacks on all
MVP endpoints, fix DI scoping for host modules.
High: move login/refresh to direct emit, extend @Audited() with
resource option, add AuditAction enum, tap vs finalize docs.
Medium: drop repo stub, fix occurredAt, add attempts:1 in code,
separate browserName/os fields, fix role claim, complete file list.
Low: clarify emit location, entity array registration, test Redis,
interceptor ordering, endpoint count.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 2 adversarial review fixes to audit trail spec

Fix 14 findings: emit audit AFTER transactions with void (fire-and-
forget), replace existing @UseInterceptors instead of adding duplicates,
AuditInterceptor falls back to JWT payload (no CurrentUserInterceptor
required), import AppClsModule instead of CommonModule, PII-safe
logging in failed handler, PostgreSQL DEFAULT on occurred_at, fix AC 4
syntax and Task 6 Zod contradiction, document empty metadata for
/ingest and null resourceId for created-by-action scenarios.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 3 adversarial review fixes to audit trail spec

Critical: add explicit transaction refactoring pattern with code
snippets for Login() and RefreshToken() — emit after transaction
returns, not inside. High: correct @optional() resilience claim,
fix resourceType for wipe endpoint, add per-endpoint interceptor
bullets, acknowledge logout MetaDataInterceptor gap. Medium: add
metadata schema table, full UUID regex, AuthenticatedRequest type,
justify @global() convention, full FileInterceptor config, fix
/ingest CurrentUserInterceptor contradiction. Low: create()+flush()
pattern, processor log counter, fix test description, base class name.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* docs: apply round 4 adversarial review fixes to audit trail spec

Fix metadata table accuracy (4 rows showed wrong shapes), add null-
safe destructure for RequestMetadataService in direct emit, strategy
Name fallback for minification, type action as AuditAction end-to-end,
consistent PII redaction policy, add CurrentUserInterceptor to POST
/submissions (highest-volume), make occurredAt required (no JS default),
document MetaDataInterceptor DI resolution, clarify attempts config,
note create-action resourceId gap, Redis failed job monitoring guidance,
actorId is plain string not @manytoone, logging side effect note.

https://claude.ai/code/session_01EXwPk6t54GxvuoMNtD1SSP

* FAC-98 feat: add audit trail MVP

Implement append-only AuditLog entity backed by a BullMQ AUDIT queue
with dual emission paths — interceptor for authenticated endpoints and
direct emit for auth events where CLS context is unavailable.

- AuditLog entity (no CustomBaseEntity, no soft delete, no FK to User)
- AuditService with fire-and-forget Emit() that never breaks requests
- @Audited() decorator + AuditInterceptor (RxJS tap, post-response)
- @global() AuditModule — cross-cutting concern, no explicit imports
- 11 MVP endpoints tagged: auth (login/logout/refresh), moodle sync,
  questionnaire (submit/ingest/wipe), analysis (create/confirm/cancel)
- Sanitized failure metadata (fixed reason codes, no raw error.message)
- Metadata size cap (4KB) on interceptor-captured params/query
- actorId index for "what did user X do?" queries
- Shared audit test helpers to DRY controller spec boilerplate
- 622 tests passing, lint clean

---------



---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FAC-98 feat: add audit trail MVP

2 participants